//
//  File.swift
//
//
//  Created by didi on 2022/9/7.
//

import Fluent
import Foundation
import Liquid
import MolecularObjects
import Vapor

public final class Molecular {
    unowned var app: Application

    public private(set) var moduleManager: MolecularModuleManager

    public struct Directories {
        public static let resources: String = "Resources"
        public static let `public`: String = "Public"
        public static let assets: String = "assets"
        public static let css: String = "css"
        public static let images: String = "img"
        public static let javascript: String = "js"
        public static let svg: String = "svg"
    }

    // paths are always absolute, with a trailing slash
    public struct Paths {
        public let base: URL

        init(_ path: String) {
            base = URL(fileURLWithPath: path)
        }

        public var resources: URL { base.appendingPathComponent(Directories.resources) }
        public var `public`: URL { base.appendingPathComponent(Directories.public) }
        public var assets: URL { `public`.appendingPathComponent(Directories.assets) }
        public var css: URL { `public`.appendingPathComponent(Directories.css) }
        public var images: URL { `public`.appendingPathComponent(Directories.images) }
        public var javascript: URL { `public`.appendingPathComponent(Directories.javascript) }
        public var svg: URL { `public`.appendingPathComponent(Directories.svg) }
    }

    public static func dateFormatter(dateStyle: DateFormatter.Style = .short, timeStyle: DateFormatter.Style = .short) -> DateFormatter {
        let formatter = DateFormatter()
        formatter.dateStyle = dateStyle
        formatter.timeStyle = timeStyle
        return formatter
    }

    public let publicUrl: String
    public let workDir: String
    public let paths: Paths

    public var installIsComplatedKey: String { "Server installed Complated" }
    public var installIsComplated: Bool = false
    public var config: Config = .default

    init(app: Application) {
        self.app = app
        moduleManager = .init([])
        workDir = DirectoryConfiguration.detect().workingDirectory
        let https = app.http.server.configuration.tlsConfiguration != nil
        let hostname = app.http.server.configuration.hostname
        let port = app.http.server.configuration.port
        publicUrl = (https ? "https" : "http") + "://" + hostname + (port != 80 ? ":\(port)" : "")
        paths = Paths(workDir)
    }

    public func boot() {
        app.directory = .init(workingDirectory: workDir)
    }

    public func start(_ userModules: [MolecularModule] = [], template: SystemModuleTemplate? = nil) throws {
        guard app.databases.configuration() != nil else {
            fatalError("Missing database configuration")
        }

        guard app.fileStorages.configuration() != nil else {
            fatalError("Missing file storage configuration")
        }

        app.hooks.register(.installVariables, use: installVariableHook)

        app.middleware.use(FileMiddleware(publicDirectory: app.directory.publicDirectory))

        app.sessions.use(.fluent)
        app.migrations.add(SessionRecord.migration)
        app.middleware.use(app.sessions.middleware)
        app.middleware.use(MolecularGuestUserAuthenticator())
        app.middleware.use(MolecularVariableMiddleware())

        moduleManager.add([SystemModule(template: template ?? DefaultSystemModuleTemplate())] + userModules)

        try createRequiredDirectories()
        try copySystemModuleBundle()
        try copyModuleBundles(moduleManager.modules)

        for module in moduleManager.modules {
            try module.boot(app)
        }
        for module in moduleManager.modules {
            try module.config(app)
        }

        try app.autoMigrate().wait()

        installIsComplated = (try app.system.variable.repository.query().filter(\.$key == installIsComplatedKey).first().wait()) != nil
    }
}

private extension Molecular {
    func installVariableHook(args: HookArguments) -> [Variable.Create] {
        [
            .init(key: installIsComplatedKey,
                  name: "Server installed",
                  notes: "Keep DB Store"),
        ]
    }

    func createRequiredDirectories() throws {
        let fm = FileManager.default
        try fm.createDirectory(at: paths.public)
        try fm.createDirectory(at: paths.resources)
    }

    func copyPublicFiles(at url: URL, to name: String) throws {
        let fm = FileManager.default
        let publicUrl = url.appendingPathComponent("Public")
        guard fm.isExistingDirectory(at: publicUrl.path) else {
            return
        }
        let publicDirectories = try fm.contentsOfDirectory(atPath: publicUrl.path)

        for dir in publicDirectories {
            let dirUrl = publicUrl.appendingPathComponent(dir)
            guard fm.isExistingDirectory(at: dirUrl.path) else {
                continue
            }
            let assetFiles = try fm.contentsOfDirectory(atPath: dirUrl.path)
            for file in assetFiles {
                let fileUrl = dirUrl.appendingPathComponent(file)
                let destDir = paths.public
                    .appendingPathComponent(dir)
                    .appendingPathComponent(name.lowercased())
                let destFile = destDir.appendingPathComponent(file)

                try fm.createDirectory(at: destDir)
                try fm.copy(at: fileUrl, to: destFile)
            }
        }
    }

    func copySystemModuleBundle() throws {
        guard let bundleUrl = Bundle.module.resourceURL?.appendingPathComponent("Bundle") else {
            return
        }

        let moduleUrl = bundleUrl.appendingPathComponent(SystemModule.uniqueKey.capitalized)
        try copyPublicFiles(at: moduleUrl, to: SystemModule.uniqueKey)
    }

    func copyModuleBundles(_ modules: [MolecularModule]) throws {
        for module in modules.filter({ type(of: $0).bundleUrl != nil }) {
            let staticModule = type(of: module)
            let moduleUrl = staticModule.bundleUrl!
            let moduleName = staticModule.uniqueKey.lowercased()
            try copyPublicFiles(at: moduleUrl, to: moduleName)
        }
    }
}
